# -*- coding: utf-8 -*-
#
# pymsn - a python client library for Msn
#
# Copyright (C) 2005-2006 Ali Sabil <ali.sabil@gmail.com>
# Copyright (C) 2006  Johann Prieur <johann.prieur@gmail.com>
# Copyright (C) 2006  Ole André Vadla Ravnås <oleavr@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""Transport
Protocol transport module.

This module contains classes used by the library to connect to the msn server
it includes for example, the direct connection transport as well as an http
polling transport ideal in firewalled environment."""

__version__ = "$Id$"

import logging
import base64
import gobject

import gio
import structure

from consts import ServerType

logger = logging.getLogger('Connection')

class BaseTransport(gobject.GObject):
    """Abstract Base Class that modelize a connection to an MSN service"""
    
    __gsignals__ = {
            "connection-failure" : (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                ()),

            "connection-success" : (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                ()),

            "connection-reset" : (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                ()),

            "connection-lost" : (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                ()),

            "command-received": (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                (object,)),

            "command-sent": (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                (object,)),
            }   

    def __init__(self, server, server_type=ServerType.NOTIFICATION, proxies={}):
        """Connection initialization
        
            @param server: the server to connect to.
            @type server: (host: string, port: integer)

            @param server_type: the server that we are connecting to, either
                Notification or switchboard.
            @type server_type: L{consts.ServerType}

            @param proxies: proxies that we can use to connect
            @type proxies: {type: string => L{gio.network.ProxyInfos}}"""
        gobject.GObject.__init__(self)
        self.server = server
        self.server_type = server_type
        self.proxies = proxies
        self._transaction_id = 0
    
    def __get_transaction_id(self):
        return self._transaction_id
    transaction_id = property(__get_transaction_id,
            doc="return the current transaction id")

    # Connection
    def establish_connection(self):
        """Connect to the server server"""
        raise NotImplementedError

    def lose_connection(self):
        """Disconnect from the server"""
        raise NotImplementedError

    def reset_connection(self, server=None):
        """Reset the connection

            @param server: when set, reset the connection and
                connect to this new server
            @type server: (host: string, port: integer)"""
        raise NotImplementedError

    # Command Sending
    def send_command(self, command, increment=True, callback=None, cb_args=()):
        """
        Sends a L{structure.Command} to the server.

            @param command: command to send
            @type command: L{structure.Command}

            @param increment: if False, the transaction ID is not incremented
            @type increment: bool

            @param callback: callback to be used when the command has been
                transmitted
            @type callback: callable

            @param cb_args: callback arguments
            @type cb_args: tuple
        """
        raise NotImplementedError

    def send_command_ex(self, command, arguments=None, payload=None,
            transaction_id=-1, increment=True, callback=None, cb_args=()):
        """
        Builds a command object then send it to the server.
        
            @param command: the command name, must be a 3 letters
                uppercase string.
            @type command: string
        
            @param arguments: command arguments
            @type arguments: (string, ...)
        
            @param payload: payload data
            @type payload: string

            @param increment: if False, the transaction ID is not incremented
            @type increment: bool

            @param callback: callback to be used when the command has been
                transmitted
            @type callback: callable

            @param cb_args: callback arguments
            @type cb_args: tuple
        """
        if transaction_id is not None and transaction_id < 0:
            transaction_id = self._transaction_id
        cmd = structure.Command()
        cmd.build(command, transaction_id, arguments, payload)
        self.send_command(cmd, increment, callback, cb_args)

    def _increment_transaction_id(self):
        """Increments the Transaction ID then return it.
            
            @rtype: integer"""
        self._transaction_id += 1
        return self._transaction_id

gobject.type_register(BaseTransport)


class DirectConnection(BaseTransport):
    """Implements a direct connection to the net without any proxy""" 

    def __init__(self, server, server_type=ServerType.NOTIFICATION, proxies={}):
        BaseTransport.__init__(self, server, server_type, proxies)
        
        transport = gio.network.TCPClient(server[0], server[1])
        transport.connect("notify::status", self.__on_status_change)
        transport.connect("error", lambda t, msg: self.emit("connection-failure"))

        receiver = gio.ChunkReceiver(transport)
        receiver.connect("received", self.__on_received)

        self._receiver = receiver
        self._receiver.delimiter = "\r\n"
        self._transport = transport
        self.__pending_chunk = None
        self.__resetting = False
        
    __init__.__doc__ = BaseTransport.__init__.__doc__

    ### public commands
    
    def establish_connection(self):
        logger.debug('<-> Connecting to %s:%d' % self.server )
        self._transport.open()

    def lose_connection(self):
        self._transport.close()

    def reset_connection(self, server=None):
        if server:
            self._transport.set_property("host", server[0])
            self._transport.set_property("port", server[1])
            self.server = server
        self.__resetting = True
        self._transport.close()
        self._transport.open()

    def send_command(self, command, increment=True, callback=None, cb_args=()):
        logger.debug('>>> ' + repr(command))
        our_cb_args = (command, callback, cb_args)
        self._transport.send(str(command), self.__on_command_sent, our_cb_args)
        if increment:
            self._increment_transaction_id()

    def __on_command_sent(self, command, user_callback, user_cb_args):
        self.emit("command-sent", command)

        if user_callback:
            user_callback(*user_cb_args)

    ### callbacks
    def __on_status_change(self, transport, param):
        status = transport.get_property("status")
        if status == gio.STATUS_OPEN:
            if self.__resetting:
                self.emit("connection-reset")
                self.__resetting = False
            self.emit("connection-success")
        elif status == gio.STATUS_CLOSED:
            if not self.__resetting:
                self.emit("connection-lost")

    def __on_received(self, receiver, chunk):
        cmd = structure.Command()
        if self.__pending_chunk:
            chunk = self.__pending_chunk + "\r\n" + chunk
            cmd.parse(chunk)
            self.__pending_chunk = None
            self._receiver.delimiter = "\r\n"
        else:
            cmd.parse(chunk)
            if cmd.name in structure.Command.PAYLOAD_COMMANDS:
                payload_len = int(cmd.arguments[-1])
                if payload_len > 0:
                    self.__pending_chunk = chunk
                    self._receiver.delimiter = payload_len
                    return
                
        logger.debug('<<< ' + repr(cmd))
        self.emit("command-received", cmd)

gobject.type_register(DirectConnection)


class HTTPPollTransportStatus(object):
    """Transport status."""
    
    OPEN = 0
    """The transport is open."""
    OPENING_REQUESTED = 1
    """The transport is being opened due to user request."""
    OPENING_AUTO = 2
    """The transport is being opened due to auto reconnection process."""
    LOST = 3
    """The transport lost his connection."""
    CLOSING = 4
    """The transport is being closed."""
    CLOSED = 5
    """The transport is closed."""

class HTTPPollTransactionStatus(object):
    """Data transfert status"""
    NONE = 0
    SENDING = 1
    RECEIVING = 2
    IGNORING = 3 # to deal with HTTP/1.1 100 Continue

class HTTPPollConnection(BaseTransport):
    """Implements an http connection to the net"""

    POLL_DELAY = 3000

    def __init__(self, server, server_type=ServerType.NOTIFICATION, proxies={}):
        self._target_host = server[0]
        server = ("gateway.messenger.hotmail.com", 80)
        BaseTransport.__init__(self, server, server_type, proxies)
        
        if 'http' in self.proxies:
            transport = gio.network.TCPClient(proxies['http'].host, proxies['http'].port)
        else:
            transport = gio.network.TCPClient(server[0], server[1])
        transport.connect("notify::status", self.__on_status_change)
        transport.connect("error", lambda t, msg: self.emit("connection-failure"))
    
        receiver = gio.ChunkReceiver(transport)
        receiver.connect("received", self.__on_received)
        
        self._receiver = receiver
        self._receiver.delimiter = "\r\n"
        self._transport = transport

        self._command_queue = []

        self._transport_status = HTTPPollTransportStatus.CLOSED
        self._transaction_status = HTTPPollTransactionStatus.NONE
        self._session_open = False          # used to send Action=open
        
        self.__clear_response_handler()

        self._poll_id = None
        self._disable_poll = True
        
    __init__.__doc__ = BaseTransport.__init__.__doc__

    ### public commands
    def establish_connection(self):
        logger.debug('<-> Connecting to %s:%d' % self.server)
        self._transport.open()

    def lose_connection(self):
        self._transport_status = HTTPPollTransportStatus.CLOSING
        self._session_open = False
        self._transaction_status = HTTPPollTransactionStatus.NONE
        self._transport.close()

    def reset_connection(self, server=None): #TODO: HTTPPollTransportStatus.RESETING ?
        if server:
            self._transport.set_property("host", server[0])
            self._transport.set_property("port", server[1])
            self.server = server
        self._transport.close()
        self._session_open = False
        self._transaction_status = HTTPPollTransactionStatus.NONE
        self._transport.open()

    def send_command(self, command, increment=True, callback=None, cb_args=()):
        self.__queue_command(command, callback, cb_args)
        if increment:
            self._increment_transaction_id()

    def __queue_command(self, command, callback, cb_args):
        self._command_queue.append((command, callback, cb_args))
        self.__process_command_queue()

    def __pop_command(self):
        return self._command_queue.pop(0)
    
    def __send_command(self, command, callback=None, cb_args=()):
        logger.debug('>>> ' + repr(command))
        host = self.server[0]
        strcmd = str(command)
        
        if not self._session_open:
            params = "Action=open&Server=%s&IP=%s" % (self.server_type, self._target_host)
            self._session_open = True
        elif command == None: # Polling the server for queued messages
            assert(self._transaction_status == HTTPPollTransactionStatus.NONE)
            params = "Action=poll&SessionID=%s" % self.session_id
            strcmd = ""
        else: # new command
            assert(self._transaction_status == HTTPPollTransactionStatus.NONE)
            params = "SessionID=%s" % self.session_id

        action = "POST http://%s/gateway/gateway.dll?%s HTTP/1.1" % (host, params)
        headers = []

        headers.append("Accept: */*")
        headers.append("Accept-Language: en-us")
        headers.append("User-Agent: MSMSGS")
        headers.append("Host: " + host)
        headers.append("Proxy-Connection: Keep-Alive")
        headers.append("Connection: Keep-Alive")
        headers.append("Pragma: no-cache")
        headers.append("Content-Type: application/x-msn-messenger")
        headers.append("Content-Length: %d" % len(strcmd))
        if 'http' in self.proxies and self.proxies['http'].user:
                auth = base64.encodestring(self.proxies['http'].user + ':' + self.proxies['http'].password)
                headers.append("Proxy-authorization: Basic " + auth) 

        http_envelope = action + "\r\n" + "\r\n".join(headers) + "\r\n"

        self._transaction_status = HTTPPollTransactionStatus.SENDING
        command_sent_cb_args = (command, callback, cb_args)
        self._transport.send(http_envelope + "\r\n" + strcmd,
                self.__on_command_sent, command_sent_cb_args)
        blob = http_envelope + "\r\n" + strcmd
        
        #for line in blob.split('\r\n'):
        #    print '\t>>> ', line
        #print '----------------------------------------------------------'

    def __process_command_queue(self):
        s = HTTPPollTransportStatus
        if self._transport_status == s.OPEN:
            if self._transaction_status != HTTPPollTransactionStatus.NONE:
                return

            if len(self._command_queue) > 0:
                cmd = self.__pop_command()
                self.__send_command(*cmd)
            elif not self._disable_poll:
                self.__send_command(None) # Poll (WARN: don't use self.__poll())
        elif self._transport_status == s.CLOSED or \
                self._transport_status == s.LOST:
            self.establish_connection()

    def __poll(self):
        if len(self._command_queue) == 0:
            self.__process_command_queue()
        return True

    ### callbacks
    def __on_command_sent(self, command, user_callback, user_cb_args):
        self.emit("command-sent", command)
        if user_callback:
            user_callback(*user_cb_args)

    def __on_status_change(self, transport, param):
        status = transport.get_property("status")
        s = HTTPPollTransportStatus
        if status == gio.STATUS_OPEN:
            if self._transport_status == s.OPENING_REQUESTED:
                self._transport_status = s.OPEN
                self.emit("connection-success")
                self._poll_id = gobject.timeout_add(self.POLL_DELAY, self.__poll)
            elif self._transport_status == s.OPENING_AUTO:
                self._transport_status = s.OPEN
                self.__process_command_queue()

        elif status == gio.STATUS_OPENING:
            if self._transport_status == s.CLOSED:
                self._transport_status = s.OPENING_REQUESTED
            elif self._transport_status == s.LOST:
                self._transport_status = s.OPENING_AUTO

        elif status == gio.STATUS_CLOSED:
            if self._transport_status == s.OPEN:
                self._transport_status = s.LOST
            elif self._transport_status == s.CLOSING:
                self._transport_status = s.CLOSED
                self.emit("connection-lost")
                self._disable_poll = True
                gobject.source_remove(self._poll_id)

    def __on_received(self, receiver, chunk):
        #print '\t<<< ', chunk
        if self._transaction_status == HTTPPollTransactionStatus.SENDING and \
                chunk[:4] == 'HTTP': # HTTP status line
            chunk = chunk.split()
            if chunk[1] == '100':
                self._transaction_status = HTTPPollTransactionStatus.IGNORING
            elif chunk[1] == '200':
                self._transaction_status = HTTPPollTransactionStatus.RECEIVING
            else:
                self.emit("connection-failure")
                self.lose_connection()
        elif not self.http_body_pending: # headers
            if len(chunk) != 0:
                header, value =  [p.strip() for p in chunk.split(':', 1)]
                if header == 'Content-Length':
                    self.content_length = int(value)
                elif header == 'X-MSN-Messenger':
                    for elem in value.split(';'):
                        key, val =  [p.strip() for p in elem.split('=', 1)]
                        if key == 'SessionID':
                            self.session_id = val
                        elif key == 'GW-IP':
                            self.server = (val, self.server[1])
                            #if 'http' not in self.proxies: # TODO: shall we reset the connection ?
                            #    self._transport.set_property("host", val)
                        elif key == 'Session'and val == 'close':
                            self.emit("connection-lost")
                            self.lose_connection()
            else: # empty line separating headers from body
                if self._transaction_status == HTTPPollTransactionStatus.IGNORING:
                    self._transaction_status = HTTPPollTransactionStatus.SENDING
                else:
                    if self.content_length > 0:
                        self.http_body_pending = True
                        self._receiver.delimiter = self.content_length
                    else:
                        self.__clear_response_handler()
        else: # HTTP body
            self.data = chunk
            if self.content_length == 0:
                # The message was an empty response to a poll,
                # there is nothing to retrieve from the server
                pass
            elif len(self.data) != 0:
                while len(self.data) != 0:
                    self.data = self.__extract_command(self.data)
                    
            # We got a complete response from the server, we can
            # send the next command or reconnect to the server
            # if necessary
            self.__clear_response_handler()
            self.__process_command_queue()

    def __extract_command(self, data):
        first, rest = data.split('\r\n', 1)
        cmd = structure.Command()
        cmd.parse(first.strip())
        if cmd.name == 'USR' and cmd.arguments[0] == 'OK':
            self._disable_poll = False
        if cmd.name in structure.Command.PAYLOAD_COMMANDS:
            payload_len = int(cmd.arguments[-1])
            if payload_len > 0:
                cmd.payload = rest[:payload_len].strip()
            logger.debug('<<< ' + repr(cmd))
            self.emit("command-received", cmd)
            return rest[payload_len:]
        else:
            logger.debug('<<< ' + repr(cmd))
            self.emit("command-received", cmd)
            return rest

    def __clear_response_handler(self):
        self.http_body_pending = False
        self.data = ''
        self.content_length = 0
        self._receiver.delimiter = '\r\n'
        self._transaction_status = HTTPPollTransactionStatus.NONE

gobject.type_register(HTTPPollConnection)
